#--
# Active Directory Module for Ruby
#
# Copyright (c) 2005-2006 Justin Mecham
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#++

module ActiveDirectory

  #
  # Represents a User object within an Active Directory instance.
  #
  class User < Base
    UAC_ACCOUNT_DISABLED  = 0x0002 # 2
    UAC_NORMAL_ACCOUNT    = 0x0200 # 512
    UAC_PASSWORD_EXPIRED  = 0x800000 # 8388608
    
    # Direct Reports
    attr :direct_reports

    # Manager
    attr :manager

    # Groups this User is a member of
    attr :groups

    extend Schema::User

    # Convenience Methods for accessing strangely named ActiveDirectory attributes

    #
    # Attempts to authenticate the loaded user with the supplied password.
    # Returns true if the authentication attempt was successful.
    #
    def authenticate(password)

      # Clean up the password before we run it through our series of tests.
      password.strip! unless password.nil?

      # If no password was specified, raise an exception. This check must
      # occur to avoid a huge security hole if anonymous bind is on - if this
      # check is not performed, someone can authenticate without providing a
      # password when anonymous bind is turned on.
      raise PasswordInvalid unless (!password.nil? and password.length > 0)

      # Clone our shared connection for isolated use in determining the
      # validity of our user's credentials.
      auth_connection = Base.connection.clone
      begin
        auth_connection.connect("#{username}@#{@@server_settings[:domain]}",password,false)
        return true
      rescue LDAP::ResultError
        if ($!.to_s == "Invalid credentials")
          raise PasswordInvalid
        else
          raise
        end
      ensure
        auth_connection.close
      end

      return false
    end

    #
    # Proxy for loading and returning this users' manager.
    #
    def manager #:nodoc:
      @manager ||= User.find(@manager_dn) unless @manager_dn.nil?
    end

    #
    # Proxy for loading and returning the group membership of this user.
    #
    def groups #:nodoc:
      # FIXME : User#groups does NO CACHING WHATSOEVER
      groups = Array.new
      unless attributes(:memberOf).nil?
        attributes(:memberOf).each do |group_dn|
          groups << Group.find(group_dn)
        end
      end
      groups
    end

    #
    # Proxy for loading and returning the users who report directly to this
    # user.
    #
    def direct_reports #:nodoc:
      direct_reports = Array.new
      unless @direct_reports.nil?
        for user_dn in @direct_reports
          direct_reports << User.new(user_dn)
        end
      end
      direct_reports
    end

    #
    # Determines if the user is a member of the given group. Returns true if
    # the user is in the passed group.
    #
    def member_of?(group)
      attributes(:memberOf).include?(group.dn)
    rescue
      false
    end

    def add_to_group(group)
      group.add_to_group(self)
    end

    def remove_from_group(group)
      group.remove_from_group(self)
    end
 
    def locked?
      attributes(:lockoutTime) != nil and attributes(:lockoutTime) != '0'
    end

    def disabled?
      attributes(:userAccountControl).to_i & UAC_ACCOUNT_DISABLED != 0
    end

    def can_login?
      not locked? and not disabled?
    end

    def change_password(new_password)
      hash = {
        'unicodePwd' => [ ActiveDirectory::Datatypes::UnicodePassword.to_remote(new_password) ],
        'lockoutTime' => [ '0' ],
        'userAccountControl' => [ UAC_NORMAL_ACCOUNT.to_s ]
      }
      Base.connection.raw.modify(dn,hash)
    end

    def unlock!
      update_attributes(:lockoutTime => 0)
    end

    protected
    def parse_ldap_attributes(entry)
      super
      @remote_attributes[:memberOf] = entry['memberOf']
    end
  end
end
